<?php
/**
 * @author 595949289@qq.com
 * @date 2024/12/27 18:43
 */

namespace CuiFox\yii\rbac;

use Yii;
use yii\base\Controller;
use yii\base\Module;
use yii\helpers\ArrayHelper;
use yii\web\Application;
use yii\base\InvalidConfigException;
use Exception;

class Route
{
    const PREFIX = '/';

    private $advanced;

    private $config;

    /**
     * Route constructor.
     * @param null $config
     * @throws Exception
     */
    public function __construct($config = null)
    {
        $value = ArrayHelper::getValue(Yii::$app->params, 'CuiFox.advanced', []);
        if ($value) {
            $this->advanced = $value;
        }

        if ($config) {
            $this->config = $config;
        }
    }

    /**
     * @return array
     * @throws InvalidConfigException
     */
    public function getRoutes()
    {
        if ($this->advanced) {
            // Create empty routes array.
            $routes = [];
            // Save original app.
            $yiiApp = Yii::$app;

            foreach ($this->advanced as $id => $paths) {
                // Create empty config array.
                $config = [];
                // Assemble configuration for current app.
                foreach ($paths as $path) {
                    // Merge every new configuration with the old config array.
                    $config = ArrayHelper::merge($config, require(Yii::getAlias($path)));
                }
                unset($config['bootstrap']);

                // Create new app using the config array.
                $app = new Application($config);
                // Get all the routes of the newly created app.
                $r = $this->getAppRoutes($app);
                // Dump new app
                unset($app);
                // Prepend the app id to all routes.
                $routes[$id] = $r;
            }

            // Switch back to original app.
            Yii::$app = $yiiApp;
            unset($yiiApp);
        } elseif ($this->config) {
            $yiiApp = Yii::$app;
            // Create empty config array.
            $config = [];
            // Assemble configuration for current app.
            foreach ($this->config as $path) {
                // Merge every new configuration with the old config array.
                $config = ArrayHelper::merge($config, require(Yii::getAlias($path)));
            }
            unset($config['bootstrap']);

            // Create new app using the config array.
            $app = new Application($config);
            // Get all the routes of the newly created app.
            $routes = $this->getAppRoutes($app);
            // Dump new app
            unset($app);
            // Switch back to original app.
            Yii::$app = $yiiApp;
            unset($yiiApp);
        } else {
            $routes = $this->getAppRoutes();
        }

        return $routes;
    }

    /**
     * @param null $module
     * @return array
     */
    public function getAppRoutes($module = null)
    {
        if ($module === null) {
            $module = Yii::$app;
        } elseif (is_string($module)) {
            $module = Yii::$app->getModule($module);
        }

        $result = [];
        $this->getRouteRecursive($module, $result);

        return $result;
    }

    /**
     * @param Module $module
     * @param $result
     */
    protected function getRouteRecursive(Module $module, &$result)
    {
        try {
            foreach ($module->getModules() as $id => $child) {
                if (($child = $module->getModule($id)) !== null && !in_array($id, ['gii', 'debug'])) {
                    $this->getRouteRecursive($child, $result);
                }
            }

            foreach ($module->controllerMap as $id => $type) {
                $this->getControllerActions($type, $id, $module, $result);
            }

            $namespace = trim($module->controllerNamespace, '\\') . '\\';
            $this->getControllerFiles($module, $namespace, '', $result);
            $all = '/' . ltrim($module->uniqueId . '/*', '/');
            $result[] = $all;
        } catch (\Exception $exc) {
            Yii::error($exc->getMessage(), __METHOD__);
        }
    }

    /**
     * @param $type
     * @param $id
     * @param $module
     * @param $result
     */
    protected function getControllerActions($type, $id, $module, &$result)
    {
        try {
            /* @var $controller \yii\base\Controller */
            $controller = Yii::createObject($type, [$id, $module]);
            $this->getActionRoutes($controller, $result);
            $all = "/{$controller->uniqueId}/*";
            $result[] = $all;
        } catch (\Exception $exc) {
            Yii::error($exc->getMessage(), __METHOD__);
        }
    }

    /**
     * @param Controller $controller
     * @param $result
     */
    protected function getActionRoutes(Controller $controller, &$result)
    {
        try {
            $prefix = '/' . $controller->uniqueId . '/';
            foreach ($controller->actions() as $id => $value) {
                $result[] = $prefix . $id;
            }
            $class = new \ReflectionClass($controller);
            foreach ($class->getMethods() as $method) {
                $name = $method->getName();
                if ($method->isPublic() && !$method->isStatic() && strpos($name,
                        'action') === 0 && $name !== 'actions') {
                    $name = strtolower(preg_replace('/(?<![A-Z])[A-Z]/', ' \0', substr($name, 6)));
                    $id = $prefix . ltrim(str_replace(' ', '-', $name), '-');
                    $result[] = $id;
                }
            }
        } catch (\Exception $exc) {
            Yii::error($exc->getMessage(), __METHOD__);
        }
    }

    /**
     * @param $module
     * @param $namespace
     * @param $prefix
     * @param $result
     */
    protected function getControllerFiles($module, $namespace, $prefix, &$result)
    {
        $path = Yii::getAlias('@' . str_replace('\\', '/', $namespace), false);
        try {
            if (!is_dir($path)) {
                return;
            }
            foreach (scandir($path) as $file) {
                if ($file == '.' || $file == '..') {
                    continue;
                }
                if (is_dir($path . '/' . $file) && preg_match('%^[a-z0-9_/]+$%i', $file . '/')) {
                    $this->getControllerFiles($module, $namespace . $file . '\\', $prefix . $file . '/', $result);
                } elseif (strcmp(substr($file, -14), 'Controller.php') === 0) {
                    $baseName = substr(basename($file), 0, -14);
                    $name = strtolower(preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $baseName));
                    $id = ltrim(str_replace(' ', '-', $name), '-');
                    $className = $namespace . $baseName . 'Controller';
                    if (strpos($className, '-') === false && class_exists($className) && is_subclass_of($className,
                            'yii\base\Controller')) {
                        $this->getControllerActions($className, $prefix . $id, $module, $result);
                    }
                }
            }
        } catch (\Exception $exc) {
            Yii::error($exc->getMessage(), __METHOD__);
        }
    }

    /**
     * @return array
     * @throws InvalidConfigException
     */
    public function getAvailable()
    {
        $exists = [];
        $routes = $this->getRoutes();
        $manager = Yii::$app->getAuthManager();
        foreach (array_keys($manager->getPermissions()) as $name) {
            $exists[] = $name;
            unset($routes[$name]);
        }
        return [
            'available' => array_keys($routes),
            'assigned' => $exists,
        ];
    }

    /**
     * @param $routes
     * @throws \Exception
     */
    public function add($routes)
    {
        $manager = Yii::$app->getAuthManager();
        foreach ($routes as $route) {
            try {
                $r = explode('?', $route);
                $item = $manager->createPermission($this->getPermissionName($route));
                if (count($r) > 1) {
                    $action = $this->getPermissionName('/' . trim($r[0], '/'));
                    if (($itemAction = $manager->getPermission($action)) === null) {
                        $itemAction = $manager->createPermission($action);
                        $manager->add($itemAction);
                    }
                    $parts = explode('&', $r[1]);
                    foreach ($parts as $part) {
                        $part = explode('=', $part);
                        $item->data['params'][$part[0]] = isset($part[1]) ? $part[1] : '';
                    }
                    $this->setDefaultRule();
                    $item->ruleName = RouteRule::RULE_NAME;
                    $manager->add($item);
                    $manager->addChild($item, $itemAction);
                } else {
                    $manager->add($item);
                }
            } catch (Exception $exc) {
                Yii::error($exc->getMessage(), __METHOD__);
            }
        }
    }

    /**
     * @param $routes
     */
    public function remove($routes)
    {
        $manager = Yii::$app->getAuthManager();
        foreach ($routes as $route) {
            try {
                $item = $manager->createPermission($this->getPermissionName($route));
                $manager->remove($item);
            } catch (Exception $exc) {
                Yii::error($exc->getMessage(), __METHOD__);
            }
        }
    }

    /**
     * @param $route
     * @return string
     */
    public function getPermissionName($route)
    {
        return self::PREFIX . trim($route, self::PREFIX);
    }

    /**
     * @throws \Exception
     */
    protected function setDefaultRule()
    {
        $manager = Yii::$app->getAuthManager();
        if ($manager->getRule(RouteRule::RULE_NAME) === null) {
            $manager->add(new RouteRule());
        }
    }
}